package editor.animation;

import animation.Animation;
import animation.AnimationStateMachine;
import animation.AnimationTree;
import animation.Character;
import com.crunch.math.Vector2f;
import com.crunch.math.Vector4f;

import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.*;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

public class AnimationStateMachinePanel extends EditorPanel {
	private JLabel treeInvalidLabel;

	private JPanel commandSetsControlsPanel;
	private JButton addButton;
	private JButton removeButton;
	private JTextField nameField;
	private JTabbedPane statesEventsTabs;
	private JList<String> statesList;
	private DefaultListModel<String> statesListModel;
	private ColoredListRenderer statesListRenderer;
	private JList<String> eventsList;
	private DefaultListModel<String> eventsListModel;
	private ColoredListRenderer eventsListRenderer;

	private HashMap<String, Float> floatVariables;
	private HashMap<String, Integer> intVariables;
	private JPanel variablesPanel;
	private JLabel floatVariablesLabel;
	private JLabel intVariablesLabel;
	private JSeparator variablesSeparator;

	private JPanel commandsControlsPanel;
	private JButton addCommandButton;
	private JButton removeCommandButton;
	private JList<String> commandsList;
	private DefaultListModel<String> commandsListModel;
	private ColoredListRenderer commandsListRenderer;

	private static class ListFlavor {}
	private static final Class FLAVOR_CLASS = ListFlavor.class;
	private static final String FLAVOR_STRING = "List";

	private JPanel commandControlsPanel;
	private JPopupMenu typePopupMenu;
	private JTextField typeField;

	// command options:

	// ANIMATION_PLAY
	private JLabel          animationPlayNodeNameLabel;
	private JTextField      animationPlayNodeNameField;
	private JCheckBox       animationPlayLoopCheckbox;
	private JLabel          animationPlaySpeedLabel;
	private JSpinner        animationPlaySpeedConstantSpinner;
	private JTextField      animationPlaySpeedVariableField;
	private JToggleButton   animationPlaySpeedVarButton;

	// ANIMATION_SET_TIME
	private JLabel          animationSetTimeNodeNameLabel;
	private JTextField      animationSetTimeNodeNameField;
	private JLabel          animationSetTimeTimeLabel;
	private JSpinner        animationSetTimeTimeConstantSpinner;
	private JTextField      animationSetTimeTimeVariableField;
	private JToggleButton   animationSetTimeTimeVarButton;

	// SELECT_SET_INDEX
	private JLabel          selectSetIndexNodeNameLabel;
	private JTextField      selectSetIndexNodeNameField;
	private JLabel          selectSetIndexIndexLabel;
	private JSpinner        selectSetIndexIndexConstantSpinner;
	private JTextField      selectSetIndexIndexVariableField;
	private JToggleButton   selectSetIndexIndexVarButton;

	// BLEND_TRANSITION_WEIGHT
	private JLabel          blendTransitionWeightNodeNameLabel;
	private JTextField      blendTransitionWeightNodeNameField;
	private JLabel          blendTransitionWeightWeightLabel;
	private JSpinner        blendTransitionWeightWeightConstantSpinner;
	private JTextField      blendTransitionWeightWeightVariableField;
	private JToggleButton   blendTransitionWeightWeightVarButton;
	private JLabel          blendTransitionWeightSpeedLabel;
	private JSpinner        blendTransitionWeightSpeedConstantSpinner;
	private JTextField      blendTransitionWeightSpeedVariableField;
	private JToggleButton   blendTransitionWeightSpeedVarButton;

	// BLEND_SET_WEIGHT
	private JLabel          blendSetWeightNodeNameLabel;
	private JTextField      blendSetWeightNodeNameField;
	private JLabel          blendSetWeightWeightLabel;
	private JSpinner        blendSetWeightWeightConstantSpinner;
	private JTextField      blendSetWeightWeightVariableField;
	private JToggleButton   blendSetWeightWeightVarButton;

	// SEQUENCE_TRANSITION_WEIGHT
	private JLabel          sequenceTransitionWeightNodeNameLabel;
	private JTextField      sequenceTransitionWeightNodeNameField;
	private JLabel          sequenceTransitionWeightIndexLabel;
	private JSpinner        sequenceTransitionWeightIndexSpinner;
	private JLabel          sequenceTransitionWeightWeightLabel;
	private JSpinner        sequenceTransitionWeightWeightConstantSpinner;
	private JTextField      sequenceTransitionWeightWeightVariableField;
	private JToggleButton   sequenceTransitionWeightWeightVarButton;
	private JLabel          sequenceTransitionWeightSpeedLabel;
	private JSpinner        sequenceTransitionWeightSpeedConstantSpinner;
	private JTextField      sequenceTransitionWeightSpeedVariableField;
	private JToggleButton   sequenceTransitionWeightSpeedVarButton;

	// SEQUENCE_SET_WEIGHT
	private JLabel          sequenceSetWeightNodeNameLabel;
	private JTextField      sequenceSetWeightNodeNameField;
	private JLabel          sequenceSetWeightIndexLabel;
	private JSpinner        sequenceSetWeightIndexSpinner;
	private JLabel          sequenceSetWeightWeightLabel;
	private JSpinner        sequenceSetWeightWeightConstantSpinner;
	private JTextField      sequenceSetWeightWeightVariableField;
	private JToggleButton   sequenceSetWeightWeightVarButton;

	// STATE_SET_ACTIVE
	private JLabel          stateSetActiveStateNameLabel;
	private JTextField      stateSetActiveStateNameField;
	private JCheckBox       stateSetActiveActiveCheckbox;

	// EVENT_TRIGGER_ACTIVE
	private JLabel          eventTriggerEventNameLabel;
	private JTextField      eventTriggerEventNameField;

	private boolean treeValid;
	private boolean stateMachineValid;

	private static final Color ACTIVE_COLOR = Color.BLACK;
	private static final Color INACTIVE_COLOR = Color.GRAY;
	private static final Color ERROR_COLOR = Color.RED;

	private static final Color NORMAL_FIELD_COLOR = Color.WHITE;
	private static final Color ERROR_FIELD_COLOR = Color.RED;

	private boolean performCallbacks = true;

	@Override
	public String getTabName() {
		return "Animation State Machine";
	}

	public AnimationStateMachinePanel(AnimationEditor editor) {
		this.editor = editor;
		GroupLayout topLayout = new GroupLayout(this);
		setLayout(topLayout);
		topLayout.setAutoCreateGaps(true);
		topLayout.setAutoCreateContainerGaps(true);

		treeInvalidLabel = new JLabel("Animation tree is invalid", JLabel.CENTER);
		treeInvalidLabel.setForeground(ERROR_COLOR);

		commandSetsControlsPanel = new JPanel();
		commandSetsControlsPanel.setBorder(BorderFactory.createEtchedBorder());
		{
			GroupLayout layout = new GroupLayout(commandSetsControlsPanel);
			commandSetsControlsPanel.setLayout(layout);
			layout.setAutoCreateGaps(true);
			layout.setAutoCreateContainerGaps(true);

			addButton = new JButton("Add ...");
			addButton.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					synchronized (AnimationStateMachinePanel.this.editor) {
						addCommandSet();
					}
				}
			});
			removeButton = new JButton("Remove ...");
			removeButton.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					synchronized (AnimationStateMachinePanel.this.editor) {
						removeCommandSet();
					}
				}
			});

			JLabel nameLabel = new JLabel("Name:");
			nameField = new JTextField();
			nameField.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					synchronized (AnimationStateMachinePanel.this.editor) {
						if (!performCallbacks) {
							return;
						}
						storeCommandSetProperties();
					}
				}
			});
			nameField.addFocusListener(new FocusListener() {
				@Override
				public void focusGained(FocusEvent e) {}

				@Override
				public void focusLost(FocusEvent e) {
					synchronized (AnimationStateMachinePanel.this.editor) {
						if (!performCallbacks) {
							return;
						}
						storeCommandSetProperties();
					}
				}
			});
			nameLabel.setLabelFor(nameField);

			statesEventsTabs = new JTabbedPane();

			statesListModel = new DefaultListModel<String>();
			statesList = new JList<String>(statesListModel);
			statesListRenderer = new ColoredListRenderer();
			statesList.setCellRenderer(statesListRenderer);
			statesList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
			statesList.addListSelectionListener(new ListSelectionListener() {
				@Override
				public void valueChanged(ListSelectionEvent e) {
					synchronized (AnimationStateMachinePanel.this.editor) {
						if (!performCallbacks) {
							return;
						}

						int sel = -1;
						for (int i = e.getFirstIndex(); sel == -1 && i <= e.getLastIndex() && i < statesListModel.size
								(); ++i) {
							if (statesList.isSelectedIndex(i)) {
								sel = i;
							}
						}

						storeCommandProperties();
						commandSelection = -1;
						boolean tempPerformCallbacks = performCallbacks;
						performCallbacks = false;
						try {
							commandsList.clearSelection();
						} finally {
							performCallbacks = tempPerformCallbacks;
						}
						setCommandControlsEnabled(false);

						storeCommandSetProperties();
						// deselect the other list
						performCallbacks = false;
						try {
							eventsList.clearSelection();
						} finally {
							performCallbacks = true;
						}
						modeSelection = 0;
						commandSetSelection = (sel < 0) ? null : statesListModel.get(sel);
						setCommandSetControlsEnabled(sel >= 0);
						if (sel >= 0) {
							loadCommandSetProperties();
						}
					}
				}
			});
			statesList.addMouseListener(new MouseListener() {
				@Override
				public void mouseClicked(MouseEvent e) {
					synchronized (AnimationStateMachinePanel.this.editor) {
						if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2 && treeValid && stateMachineValid) {
							// toggle state activity
							int index = statesList.locationToIndex(e.getPoint());
							if (index < 0) {
								return;
							}
							boolean active = AnimationStateMachinePanel.this.editor.state.isStateActive(
									statesListModel.get(index));
							AnimationStateMachinePanel.this.editor.state.setStateActive(
									statesListModel.get(index), !active);
							setStateActiveColors();
						}
					}
				}

				public void mousePressed(MouseEvent e) {}
				public void mouseReleased(MouseEvent e) {}
				public void mouseEntered(MouseEvent e) {}
				public void mouseExited(MouseEvent e) {}
			});
			JScrollPane statesListScroll = new JScrollPane(statesList);

			eventsListModel = new DefaultListModel<String>();
			eventsList = new JList<String>(eventsListModel);
			eventsListRenderer = new ColoredListRenderer();
			eventsList.setCellRenderer(eventsListRenderer);
			eventsList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
			eventsList.addListSelectionListener(new ListSelectionListener() {
				@Override
				public void valueChanged(ListSelectionEvent e) {
					synchronized (AnimationStateMachinePanel.this.editor) {
						if (!performCallbacks) {
							return;
						}

						int sel = -1;
						for (int i = e.getFirstIndex(); sel == -1 && i <= e.getLastIndex() && i < eventsListModel.size(); ++i) {
							if (eventsList.isSelectedIndex(i)) {
								sel = i;
							}
						}

						storeCommandProperties();
						commandSelection = -1;
						boolean tempPerformCallbacks = performCallbacks;
						performCallbacks = false;
						try {
							commandsList.clearSelection();
						} finally {
							performCallbacks = tempPerformCallbacks;
						}
						setCommandControlsEnabled(false);

						storeCommandSetProperties();
						// deselect the other list
						performCallbacks = false;
						try {
							statesList.clearSelection();
						} finally {
							performCallbacks = true;
						}
						modeSelection = 1;
						commandSetSelection = (sel < 0) ? null : eventsListModel.get(sel);
						setCommandSetControlsEnabled(sel >= 0);
						if (sel >= 0) {
							loadCommandSetProperties();
						}
					}
				}
			});
			eventsList.addMouseListener(new MouseListener() {
				@Override
				public void mouseClicked(MouseEvent e) {
					synchronized (AnimationStateMachinePanel.this.editor) {
						if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2 && treeValid && stateMachineValid) {
							// toggle state activity
							int index = eventsList.locationToIndex(e.getPoint());
							if (index < 0) {
								return;
							}
							AnimationStateMachinePanel.this.editor.state.triggerEvent(statesListModel.get(index));
							setStateActiveColors();
						}
					}
				}

				public void mousePressed(MouseEvent e) {}
				public void mouseReleased(MouseEvent e) {}
				public void mouseEntered(MouseEvent e) {}
				public void mouseExited(MouseEvent e) {}
			});
			JScrollPane eventsListScroll = new JScrollPane(eventsList);

			variablesPanel = new JPanel();
			floatVariables = new HashMap<String, Float>();
			intVariables = new HashMap<String, Integer>();
			floatVariablesLabel = new JLabel("Floats:");
			intVariablesLabel = new JLabel("Ints:");
			variablesSeparator = new JSeparator();
			rebuildVariables();

			JScrollPane variablesPanelScroll = new JScrollPane(variablesPanel);

			statesEventsTabs.addTab("States", statesListScroll);
			statesEventsTabs.addTab("Events", eventsListScroll);
			statesEventsTabs.addTab("Variables", variablesPanelScroll);
			statesEventsTabs.addChangeListener(new ChangeListener() {
				@Override
				public void stateChanged(ChangeEvent e) {
					if (statesEventsTabs.getSelectedIndex() == 0) {
						addButton.setText("Add state");
						removeButton.setText("Remove state");
					} else if (statesEventsTabs.getSelectedIndex() == 1) {
						addButton.setText("Add event");
						removeButton.setText("Remove event");
					} else {
						assert(statesEventsTabs.getSelectedIndex() == 2);
					}
					addButton.setVisible(statesEventsTabs.getSelectedIndex() != 2);
					removeButton.setVisible(statesEventsTabs.getSelectedIndex() != 2);

					// refresh GUI
					setCommandSetControlsEnabled(commandSetSelection != null);
				}
			});

			layout.setHorizontalGroup(layout.createParallelGroup()
					.addGroup(layout.createSequentialGroup()
							.addComponent(addButton)
							.addComponent(removeButton)
							.addComponent(nameLabel)
							.addComponent(nameField))
					.addComponent(statesEventsTabs));
			layout.setVerticalGroup(layout.createSequentialGroup()
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(addButton)
							.addComponent(removeButton)
							.addComponent(nameLabel)
							.addComponent(nameField))
					.addComponent(statesEventsTabs));
		}

		commandsControlsPanel = new JPanel();
		commandsControlsPanel.setBorder(BorderFactory.createEtchedBorder());
		{
			GroupLayout layout = new GroupLayout(commandsControlsPanel);
			commandsControlsPanel.setLayout(layout);
			layout.setAutoCreateGaps(true);
			layout.setAutoCreateContainerGaps(true);

			addCommandButton = new JButton("Add command");
			addCommandButton.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					synchronized (AnimationStateMachinePanel.this.editor) {
						addCommand();
					}
				}
			});
			removeCommandButton = new JButton("Remove command");
			removeCommandButton.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					synchronized (AnimationStateMachinePanel.this.editor) {
						removeCommand();
					}
				}
			});

			commandsListModel = new DefaultListModel<String>();
			commandsList = new JList<String>(commandsListModel);
			commandsListRenderer = new ColoredListRenderer();
			commandsList.setCellRenderer(commandsListRenderer);
			commandsList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
			commandsList.setDropMode(DropMode.INSERT);
			commandsList.setDragEnabled(true);
			commandsList.setTransferHandler(new TransferHandler() {
				class TransferableList implements Transferable {
					public TransferableList(Object data) {
						this.data = data;
					}

					private Object data;

					@Override
					public DataFlavor[] getTransferDataFlavors() {
						return new DataFlavor[] { new DataFlavor(FLAVOR_CLASS, FLAVOR_STRING) };
					}

					@Override
					public boolean isDataFlavorSupported(DataFlavor flavor) {
						return flavor.getRepresentationClass() == FLAVOR_CLASS;
					}

					@Override
					public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
						if (isDataFlavorSupported(flavor)) {
							return data;
						} else {
							throw new UnsupportedFlavorException(flavor);
						}
					}
				}

				@Override
				public boolean canImport(TransferSupport info) {
					return info.isDataFlavorSupported(new DataFlavor(FLAVOR_CLASS, FLAVOR_STRING));
				}

				@Override
				protected Transferable createTransferable(JComponent c) {
					Integer index = commandsList.getSelectedIndex();
					if (index < 0) {
						return null;
					}
					return new TransferableList(index);
				}

				@Override
				public int getSourceActions(JComponent c) {
					return TransferHandler.MOVE;
				}

				@Override
				public boolean importData(TransferSupport info) {
					if (!info.isDrop()) {
						return false;
					}
					JList.DropLocation dl = (JList.DropLocation) info.getDropLocation();
					int index = dl.getIndex();
					boolean insert = dl.isInsert();

					Transferable t = info.getTransferable();
					Integer idx;
					try {
						idx = (Integer) (t.getTransferData(new DataFlavor(FLAVOR_CLASS, FLAVOR_STRING)));
					} catch (UnsupportedFlavorException e) {
						return false;
					} catch (IOException e) {
						return false;
					}

					if (!insert) {
						return false;
					}

					synchronized (AnimationStateMachinePanel.this.editor) {
						moveCommand(idx, index);
						return true;
					}
				}

				@Override
				public void exportDone(JComponent c, Transferable data, int action) {
					// ...
				}
			});
			commandsList.addListSelectionListener(new ListSelectionListener() {
				@Override
				public void valueChanged(ListSelectionEvent e) {
					synchronized (AnimationStateMachinePanel.this.editor) {
						if (!performCallbacks) {
							return;
						}

						int sel = -1;
						for (int i = e.getFirstIndex(); sel == -1 && i <= e.getLastIndex() && i < commandsListModel.size(); ++i) {
							if (commandsList.isSelectedIndex(i)) {
								sel = i;
							}
						}

						storeCommandProperties();
						commandSelection = sel;
						setCommandControlsEnabled(sel >= 0);
						if (sel >= 0) {
							loadCommandProperties();
						}
					}
				}
			});
			JScrollPane commandsListScroll = new JScrollPane(commandsList);

			layout.setHorizontalGroup(layout.createParallelGroup()
					.addGroup(layout.createSequentialGroup()
							.addComponent(addCommandButton)
							.addComponent(removeCommandButton))
					.addComponent(commandsListScroll));
			layout.setVerticalGroup(layout.createSequentialGroup()
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(addCommandButton)
							.addComponent(removeCommandButton))
					.addComponent(commandsListScroll));
		}

		commandControlsPanel = new JPanel();
		commandControlsPanel.setBorder(BorderFactory.createEtchedBorder());
		{
			GroupLayout layout = new GroupLayout(commandControlsPanel);
			commandControlsPanel.setLayout(layout);
			layout.setAutoCreateGaps(true);
			layout.setAutoCreateContainerGaps(true);

			JLabel typeLabel = new JLabel("Type:");
			typeField = new JTextField();
			typeField.setEditable(false);
			typeLabel.setLabelFor(typeField);

			typePopupMenu = new JPopupMenu();
			for (int i = 0; i < AnimationStateMachine.Command.Type.values().length; ++i) {
				JMenuItem typeItem = new JMenuItem(AnimationStateMachine.Command.Type.values()[i].toString());
				final int index = i;
				typeItem.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent e) {
						synchronized (AnimationStateMachinePanel.this.editor) {
							setCommandType(AnimationStateMachine.Command.Type.values()[index]);
						}
					}
				});
				typePopupMenu.add(typeItem);
			}

			typeField.addMouseListener(new MouseListener() {
				@Override
				public void mouseClicked(MouseEvent e) {
					if (typeField.isEnabled() && e.getButton() == MouseEvent.BUTTON1) {
						typePopupMenu.show(e.getComponent(), e.getX(), e.getY());
					}
				}

				public void mousePressed(MouseEvent e) {
				}

				public void mouseReleased(MouseEvent e) {
				}

				public void mouseEntered(MouseEvent e) {
				}

				public void mouseExited(MouseEvent e) {
				}
			});

			GroupLayout.Group horizGroup = layout.createParallelGroup()
					.addGroup(layout.createSequentialGroup()
							.addComponent(typeLabel)
							.addComponent(typeField));
			GroupLayout.Group vertGroup = layout.createSequentialGroup()
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(typeLabel)
							.addComponent(typeField));

			ChangeListener changeListener = new ChangeListener() {
				@Override
				public void stateChanged(ChangeEvent e) {
					synchronized (AnimationStateMachinePanel.this.editor) {
						if (!performCallbacks) {
							return;
						}
						storeCommandProperties();
					}
				}
			};

			// ANIMATION_PLAY
			animationPlayNodeNameLabel = new JLabel("Node name:");
			animationPlayNodeNameField = new JTextField();
			setupNodeSelectionField(animationPlayNodeNameField, AnimationTree.Node.Type.ANIMATION);

			animationPlayLoopCheckbox = new JCheckBox("Loop");
			animationPlayLoopCheckbox.addChangeListener(changeListener);

			animationPlaySpeedLabel = new JLabel("Speed:");
			animationPlaySpeedConstantSpinner = new JSpinner(new SpinnerNumberModel(new Float(1.0f), new Float(-100000.0f), new Float(100000.0f), new Float(1.0f)));
			animationPlaySpeedVariableField = new JTextField();
			animationPlaySpeedVarButton = new JToggleButton("Var");
			setupBindableFields(animationPlaySpeedConstantSpinner, animationPlaySpeedVariableField, animationPlaySpeedVarButton);

			horizGroup.addGroup(layout.createSequentialGroup()
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING)
							.addComponent(animationPlayNodeNameLabel)
							.addComponent(animationPlaySpeedLabel))
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
							.addComponent(animationPlayNodeNameField)
							.addGroup(layout.createSequentialGroup()
									.addComponent(animationPlaySpeedConstantSpinner)
									.addComponent(animationPlaySpeedVariableField)
									.addComponent(animationPlaySpeedVarButton))
							.addComponent(animationPlayLoopCheckbox)));

			vertGroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
					.addComponent(animationPlayNodeNameLabel)
					.addComponent(animationPlayNodeNameField));
			vertGroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
					.addComponent(animationPlaySpeedLabel)
					.addComponent(animationPlaySpeedConstantSpinner)
					.addComponent(animationPlaySpeedVariableField)
					.addComponent(animationPlaySpeedVarButton));
			vertGroup.addComponent(animationPlayLoopCheckbox);

			// ANIMATION_SET_TIME
			animationSetTimeNodeNameLabel = new JLabel("Node name:");
			animationSetTimeNodeNameField = new JTextField();
			setupNodeSelectionField(animationSetTimeNodeNameField, AnimationTree.Node.Type.ANIMATION);

			animationSetTimeTimeLabel = new JLabel("Time:");
			animationSetTimeTimeConstantSpinner = new JSpinner(new SpinnerNumberModel(new Float(0.0f), new Float(0.0), new Float(100000.0f), new Float(1.0f)));
			animationSetTimeTimeVariableField = new JTextField();
			animationSetTimeTimeVarButton = new JToggleButton("Var");
			setupBindableFields(animationSetTimeTimeConstantSpinner, animationSetTimeTimeVariableField, animationSetTimeTimeVarButton);

			horizGroup.addGroup(layout.createSequentialGroup()
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING)
							.addComponent(animationSetTimeNodeNameLabel)
							.addComponent(animationSetTimeTimeLabel))
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
							.addComponent(animationSetTimeNodeNameField)
							.addGroup(layout.createSequentialGroup()
									.addComponent(animationSetTimeTimeConstantSpinner)
									.addComponent(animationSetTimeTimeVariableField)
									.addComponent(animationSetTimeTimeVarButton))));

			vertGroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
					.addComponent(animationSetTimeNodeNameLabel)
					.addComponent(animationSetTimeNodeNameField));
			vertGroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
					.addComponent(animationSetTimeTimeLabel)
					.addComponent(animationSetTimeTimeConstantSpinner)
					.addComponent(animationSetTimeTimeVariableField)
					.addComponent(animationSetTimeTimeVarButton));

			// SELECT_SET_INDEX
			selectSetIndexNodeNameLabel = new JLabel("Node name:");
			selectSetIndexNodeNameField = new JTextField();
			setupNodeSelectionField(selectSetIndexNodeNameField, AnimationTree.Node.Type.SELECT);

			selectSetIndexIndexLabel = new JLabel("Index:");
			selectSetIndexIndexConstantSpinner = new JSpinner(new SpinnerNumberModel(0, -1, 100000, 1));
			selectSetIndexIndexVariableField = new JTextField();
			selectSetIndexIndexVarButton = new JToggleButton("Var");
			setupBindableFields(selectSetIndexIndexConstantSpinner, selectSetIndexIndexVariableField, selectSetIndexIndexVarButton);

			horizGroup.addGroup(layout.createSequentialGroup()
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING)
							.addComponent(selectSetIndexNodeNameLabel)
							.addComponent(selectSetIndexIndexLabel))
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
							.addComponent(selectSetIndexNodeNameField)
							.addGroup(layout.createSequentialGroup()
									.addComponent(selectSetIndexIndexConstantSpinner)
									.addComponent(selectSetIndexIndexVariableField)
									.addComponent(selectSetIndexIndexVarButton))));

			vertGroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
					.addComponent(selectSetIndexNodeNameLabel)
					.addComponent(selectSetIndexNodeNameField));
			vertGroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
					.addComponent(selectSetIndexIndexLabel)
					.addComponent(selectSetIndexIndexConstantSpinner)
					.addComponent(selectSetIndexIndexVariableField)
					.addComponent(selectSetIndexIndexVarButton));

			// BLEND_TRANSITION_WEIGHT
			blendTransitionWeightNodeNameLabel = new JLabel("Node name:");
			blendTransitionWeightNodeNameField = new JTextField();
			setupNodeSelectionField(blendTransitionWeightNodeNameField, AnimationTree.Node.Type.BLEND);

			blendTransitionWeightWeightLabel = new JLabel("Weight:");
			blendTransitionWeightWeightConstantSpinner = new JSpinner(new SpinnerNumberModel(new Float(0.0f), new Float(0.0), new Float(100000.0f), new Float(1.0f)));
			blendTransitionWeightWeightVariableField = new JTextField();
			blendTransitionWeightWeightVarButton = new JToggleButton("Var");
			setupBindableFields(blendTransitionWeightWeightConstantSpinner, blendTransitionWeightWeightVariableField, blendTransitionWeightWeightVarButton);

			blendTransitionWeightSpeedLabel = new JLabel("Speed:");
			blendTransitionWeightSpeedConstantSpinner = new JSpinner(new SpinnerNumberModel(new Float(0.0f), new Float(0.0), new Float(100000.0f), new Float(1.0f)));
			blendTransitionWeightSpeedVariableField = new JTextField();
			blendTransitionWeightSpeedVarButton = new JToggleButton("Var");
			setupBindableFields(blendTransitionWeightSpeedConstantSpinner, blendTransitionWeightSpeedVariableField, blendTransitionWeightSpeedVarButton);

			horizGroup.addGroup(layout.createSequentialGroup()
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING)
							.addComponent(blendTransitionWeightNodeNameLabel)
							.addComponent(blendTransitionWeightWeightLabel)
							.addComponent(blendTransitionWeightSpeedLabel))
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
							.addComponent(blendTransitionWeightNodeNameField)
							.addGroup(layout.createSequentialGroup()
									.addComponent(blendTransitionWeightWeightConstantSpinner)
									.addComponent(blendTransitionWeightWeightVariableField)
									.addComponent(blendTransitionWeightWeightVarButton))
							.addGroup(layout.createSequentialGroup()
									.addComponent(blendTransitionWeightSpeedConstantSpinner)
									.addComponent(blendTransitionWeightSpeedVariableField)
									.addComponent(blendTransitionWeightSpeedVarButton))));

			vertGroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
					.addComponent(blendTransitionWeightNodeNameLabel)
					.addComponent(blendTransitionWeightNodeNameField));
			vertGroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
					.addComponent(blendTransitionWeightWeightLabel)
					.addComponent(blendTransitionWeightWeightConstantSpinner)
					.addComponent(blendTransitionWeightWeightVariableField)
					.addComponent(blendTransitionWeightWeightVarButton));
			vertGroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
					.addComponent(blendTransitionWeightSpeedLabel)
					.addComponent(blendTransitionWeightSpeedConstantSpinner)
					.addComponent(blendTransitionWeightSpeedVariableField)
					.addComponent(blendTransitionWeightSpeedVarButton));

			// BLEND_SET_WEIGHT
			blendSetWeightNodeNameLabel = new JLabel("Node name:");
			blendSetWeightNodeNameField = new JTextField();
			setupNodeSelectionField(blendSetWeightNodeNameField, AnimationTree.Node.Type.BLEND);

			blendSetWeightWeightLabel = new JLabel("Weight:");
			blendSetWeightWeightConstantSpinner = new JSpinner(new SpinnerNumberModel(new Float(0.0f), new Float(0.0), new Float(100000.0f), new Float(1.0f)));
			blendSetWeightWeightVariableField = new JTextField();
			blendSetWeightWeightVarButton = new JToggleButton("Var");
			setupBindableFields(blendSetWeightWeightConstantSpinner, blendSetWeightWeightVariableField, blendSetWeightWeightVarButton);

			horizGroup.addGroup(layout.createSequentialGroup()
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING)
							.addComponent(blendSetWeightNodeNameLabel)
							.addComponent(blendSetWeightWeightLabel))
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
							.addComponent(blendSetWeightNodeNameField)
							.addGroup(layout.createSequentialGroup()
									.addComponent(blendSetWeightWeightConstantSpinner)
									.addComponent(blendSetWeightWeightVariableField)
									.addComponent(blendSetWeightWeightVarButton))));

			vertGroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
					.addComponent(blendSetWeightNodeNameLabel)
					.addComponent(blendSetWeightNodeNameField));
			vertGroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
					.addComponent(blendSetWeightWeightLabel)
					.addComponent(blendSetWeightWeightConstantSpinner)
					.addComponent(blendSetWeightWeightVariableField)
					.addComponent(blendSetWeightWeightVarButton));

			// SEQUENCE_TRANSITION_WEIGHT
			sequenceTransitionWeightNodeNameLabel = new JLabel("Node name:");
			sequenceTransitionWeightNodeNameField = new JTextField();
			setupNodeSelectionField(sequenceTransitionWeightNodeNameField, AnimationTree.Node.Type.SEQUENCE);

			sequenceTransitionWeightIndexLabel = new JLabel("Index:");
			sequenceTransitionWeightIndexSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 100000, 1));
			sequenceTransitionWeightIndexSpinner.addChangeListener(changeListener);

			sequenceTransitionWeightWeightLabel = new JLabel("Weight:");
			sequenceTransitionWeightWeightConstantSpinner = new JSpinner(new SpinnerNumberModel(new Float(0.0f), new Float(0.0), new Float(100000.0f), new Float(1.0f)));
			sequenceTransitionWeightWeightVariableField = new JTextField();
			sequenceTransitionWeightWeightVarButton = new JToggleButton("Var");
			setupBindableFields(sequenceTransitionWeightWeightConstantSpinner, sequenceTransitionWeightWeightVariableField, sequenceTransitionWeightWeightVarButton);

			sequenceTransitionWeightSpeedLabel = new JLabel("Speed:");
			sequenceTransitionWeightSpeedConstantSpinner = new JSpinner(new SpinnerNumberModel(new Float(0.0f), new Float(0.0), new Float(100000.0f), new Float(1.0f)));
			sequenceTransitionWeightSpeedVariableField = new JTextField();
			sequenceTransitionWeightSpeedVarButton = new JToggleButton("Var");
			setupBindableFields(sequenceTransitionWeightSpeedConstantSpinner, sequenceTransitionWeightSpeedVariableField, sequenceTransitionWeightSpeedVarButton);

			horizGroup.addGroup(layout.createSequentialGroup()
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING)
							.addComponent(sequenceTransitionWeightNodeNameLabel)
							.addComponent(sequenceTransitionWeightIndexLabel)
							.addComponent(sequenceTransitionWeightWeightLabel)
							.addComponent(sequenceTransitionWeightSpeedLabel))
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
							.addComponent(sequenceTransitionWeightNodeNameField)
							.addComponent(sequenceTransitionWeightIndexSpinner)
							.addGroup(layout.createSequentialGroup()
									.addComponent(sequenceTransitionWeightWeightConstantSpinner)
									.addComponent(sequenceTransitionWeightWeightVariableField)
									.addComponent(sequenceTransitionWeightWeightVarButton))
							.addGroup(layout.createSequentialGroup()
									.addComponent(sequenceTransitionWeightSpeedConstantSpinner)
									.addComponent(sequenceTransitionWeightSpeedVariableField)
									.addComponent(sequenceTransitionWeightSpeedVarButton))));

			vertGroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
					.addComponent(sequenceTransitionWeightNodeNameLabel)
					.addComponent(sequenceTransitionWeightNodeNameField));
			vertGroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
					.addComponent(sequenceTransitionWeightIndexLabel)
					.addComponent(sequenceTransitionWeightIndexSpinner));
			vertGroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
					.addComponent(sequenceTransitionWeightWeightLabel)
					.addComponent(sequenceTransitionWeightWeightConstantSpinner)
					.addComponent(sequenceTransitionWeightWeightVariableField)
					.addComponent(sequenceTransitionWeightWeightVarButton));
			vertGroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
					.addComponent(sequenceTransitionWeightSpeedLabel)
					.addComponent(sequenceTransitionWeightSpeedConstantSpinner)
					.addComponent(sequenceTransitionWeightSpeedVariableField)
					.addComponent(sequenceTransitionWeightSpeedVarButton));

			// SEQUENCE_SET_WEIGHT
			sequenceSetWeightNodeNameLabel = new JLabel("Node name:");
			sequenceSetWeightNodeNameField = new JTextField();
			setupNodeSelectionField(sequenceSetWeightNodeNameField, AnimationTree.Node.Type.SEQUENCE);

			sequenceSetWeightIndexLabel = new JLabel("Index:");
			sequenceSetWeightIndexSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 100000, 1));
			sequenceSetWeightIndexSpinner.addChangeListener(changeListener);

			sequenceSetWeightWeightLabel = new JLabel("Weight:");
			sequenceSetWeightWeightConstantSpinner = new JSpinner(new SpinnerNumberModel(new Float(0.0f), new Float(0.0), new Float(100000.0f), new Float(1.0f)));
			sequenceSetWeightWeightVariableField = new JTextField();
			sequenceSetWeightWeightVarButton = new JToggleButton("Var");
			setupBindableFields(sequenceSetWeightWeightConstantSpinner, sequenceSetWeightWeightVariableField, sequenceSetWeightWeightVarButton);

			horizGroup.addGroup(layout.createSequentialGroup()
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING)
							.addComponent(sequenceSetWeightNodeNameLabel)
							.addComponent(sequenceSetWeightIndexLabel)
							.addComponent(sequenceSetWeightWeightLabel))
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
							.addComponent(sequenceSetWeightNodeNameField)
							.addComponent(sequenceSetWeightIndexSpinner)
							.addGroup(layout.createSequentialGroup()
									.addComponent(sequenceSetWeightWeightConstantSpinner)
									.addComponent(sequenceSetWeightWeightVariableField)
									.addComponent(sequenceSetWeightWeightVarButton))));

			vertGroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
					.addComponent(sequenceSetWeightNodeNameLabel)
					.addComponent(sequenceSetWeightNodeNameField));
			vertGroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
					.addComponent(sequenceSetWeightIndexLabel)
					.addComponent(sequenceSetWeightIndexSpinner));
			vertGroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
					.addComponent(sequenceSetWeightWeightLabel)
					.addComponent(sequenceSetWeightWeightConstantSpinner)
					.addComponent(sequenceSetWeightWeightVariableField)
					.addComponent(sequenceSetWeightWeightVarButton));

			// STATE_SET_ACTIVE
			stateSetActiveStateNameLabel = new JLabel("State name:");
			stateSetActiveStateNameField = new JTextField();
			stateSetActiveActiveCheckbox = new JCheckBox("Active");
			stateSetActiveActiveCheckbox.addChangeListener(changeListener);
			setupStateSelectionField(stateSetActiveStateNameField);

			horizGroup.addGroup(layout.createSequentialGroup()
					.addComponent(stateSetActiveStateNameLabel)
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
							.addComponent(stateSetActiveStateNameField)
							.addComponent(stateSetActiveActiveCheckbox)));

			vertGroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
					.addComponent(stateSetActiveStateNameLabel)
					.addComponent(stateSetActiveStateNameField));
			vertGroup.addComponent(stateSetActiveActiveCheckbox);

			// EVENT_TRIGGER_ACTIVE
			eventTriggerEventNameLabel = new JLabel("Event name:");
			eventTriggerEventNameField = new JTextField();
			setupEventSelectionField(eventTriggerEventNameField);

			horizGroup.addGroup(layout.createSequentialGroup()
					.addComponent(eventTriggerEventNameLabel)
					.addComponent(eventTriggerEventNameField));

			vertGroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
					.addComponent(eventTriggerEventNameLabel)
					.addComponent(eventTriggerEventNameField));

			layout.setHorizontalGroup(horizGroup);
			layout.setVerticalGroup(vertGroup);
		}

		topLayout.setHorizontalGroup(topLayout.createParallelGroup()
				.addComponent(treeInvalidLabel, GroupLayout.Alignment.CENTER)
				.addComponent(commandSetsControlsPanel)
				.addComponent(commandsControlsPanel)
				.addComponent(commandControlsPanel));
		topLayout.setVerticalGroup(topLayout.createSequentialGroup()
				.addComponent(treeInvalidLabel)
				.addComponent(commandSetsControlsPanel)
				.addComponent(commandsControlsPanel)
				.addComponent(commandControlsPanel));
	}

	private String selectTreeNode(AnimationTree.Node.Type type) {
		int nodeCount = countTreeNodes(editor.animationTree.getRoot(), type);
		Object[] list = new Object[nodeCount];
		addTreeNodes(editor.animationTree.getRoot(), type, list, 0);
		return (String) JOptionPane.showInputDialog(
				this,
				"Choose node:",
				"Node Selector",
				JOptionPane.PLAIN_MESSAGE,
				null,
				list,
				null);
	}

	private int countTreeNodes(AnimationTree.Node node, AnimationTree.Node.Type type) {
		int count = (node.getType() == type) ? 1 : 0;
		for (int i = 0; i < node.getChildCount(); ++i) {
			count += countTreeNodes(node.getChild(i), type);
		}
		return count;
	}

	private int addTreeNodes(AnimationTree.Node node, AnimationTree.Node.Type type, Object[] nodeNames, int pos) {
		if (node.getType() == type) {
			nodeNames[pos] = node.getName();
			++pos;
		}
		for (int i = 0; i < node.getChildCount(); ++i) {
			pos = addTreeNodes(node.getChild(i), type, nodeNames, pos);
		}
		return pos;
	}

	private String selectState() {
		Object[] list = editor.animationStateMachine.getStates().toArray();
		return (String) JOptionPane.showInputDialog(
				this,
				"Choose state:",
				"State Selector",
				JOptionPane.PLAIN_MESSAGE,
				null,
				list,
				null);
	}

	private String selectEvent() {
		Object[] list = editor.animationStateMachine.getEvents().toArray();
		return (String) JOptionPane.showInputDialog(
				this,
				"Choose event:",
				"Event Selector",
				JOptionPane.PLAIN_MESSAGE,
				null,
				list,
				null);
	}

	void setupNodeSelectionField(final JTextField field, final AnimationTree.Node.Type type) {
		field.setEditable(false);
		field.addMouseListener(new MouseListener() {
			@Override
			public void mouseClicked(MouseEvent e) {
				synchronized (AnimationStateMachinePanel.this.editor) {
					if (!performCallbacks) {
						return;
					}
					String sel = selectTreeNode(type);
					if (sel != null) {
						field.setText(sel);
						storeCommandProperties();
					}
				}
			}

			public void mousePressed(MouseEvent e) {}
			public void mouseReleased(MouseEvent e) {}
			public void mouseEntered(MouseEvent e) {}
			public void mouseExited(MouseEvent e) {}
		});
	}

	void setupStateSelectionField(final JTextField field) {
		field.setEditable(false);
		field.addMouseListener(new MouseListener() {
			@Override
			public void mouseClicked(MouseEvent e) {
				synchronized (AnimationStateMachinePanel.this.editor) {
					if (!performCallbacks) {
						return;
					}
					String sel = selectState();
					if (sel != null) {
						field.setText(sel);
						storeCommandProperties();
					}
				}
			}

			public void mousePressed(MouseEvent e) {
			}

			public void mouseReleased(MouseEvent e) {
			}

			public void mouseEntered(MouseEvent e) {
			}

			public void mouseExited(MouseEvent e) {
			}
		});
	}

	void setupEventSelectionField(final JTextField field) {
		field.setEditable(false);
		field.addMouseListener(new MouseListener() {
			@Override
			public void mouseClicked(MouseEvent e) {
				synchronized (AnimationStateMachinePanel.this.editor) {
					if (!performCallbacks) {
						return;
					}
					String sel = selectEvent();
					if (sel != null) {
						field.setText(sel);
						storeCommandProperties();
					}
				}
			}

			public void mousePressed(MouseEvent e) {
			}

			public void mouseReleased(MouseEvent e) {
			}

			public void mouseEntered(MouseEvent e) {
			}

			public void mouseExited(MouseEvent e) {
			}
		});
	}

	void setupBindableFields(final JSpinner constantSpinner, final JTextField variableField,
	                         final JToggleButton bindButton) {
		final Number defValue = (Number) constantSpinner.getValue(); // store initial value
		constantSpinner.addChangeListener(new ChangeListener() {
			@Override
			public void stateChanged(ChangeEvent e) {
				synchronized (AnimationStateMachinePanel.this.editor) {
					if (!performCallbacks) {
						return;
					}
					storeCommandProperties();
				}
			}
		});
		variableField.addFocusListener(new FocusListener() {
			@Override
			public void focusGained(FocusEvent e) {
			}

			@Override
			public void focusLost(FocusEvent e) {
				synchronized (AnimationStateMachinePanel.this.editor) {
					if (!performCallbacks) {
						return;
					}
					storeCommandProperties();
				}
			}
		});
		variableField.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				synchronized (AnimationStateMachinePanel.this.editor) {
					if (!performCallbacks) {
						return;
					}
					storeCommandProperties();
				}
			}
		});

		bindButton.addChangeListener(new ChangeListener() {
			@Override
			public void stateChanged(ChangeEvent e) {
				synchronized (AnimationStateMachinePanel.this.editor) {
					if (!performCallbacks) {
						return;
					}
					// temporarily disable callbacks to change values of fields
					performCallbacks = false;
					constantSpinner.setValue(defValue);
					variableField.setText("");
					performCallbacks = true;
					constantSpinner.setVisible(!bindButton.isSelected());
					variableField.setVisible(bindButton.isSelected());
					storeCommandProperties();
				}
			}
		});
	}

	@Override
	public void select() {
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			// clear the states/events lists and build them up
			statesList.clearSelection();
			statesListRenderer.clear();
			statesListModel.clear();
			eventsList.clearSelection();
			eventsListRenderer.clear();
			eventsListModel.clear();

			// add states/events
			for (String s : editor.animationStateMachine.getStates()) {
				statesListRenderer.add(statesListModel.getSize(), ACTIVE_COLOR);
				statesListModel.addElement(s);
				validateState(statesListModel.getSize() - 1);
			}
			for (String s : editor.animationStateMachine.getEvents()) {
				eventsListRenderer.add(eventsListModel.getSize(), ACTIVE_COLOR);
				eventsListModel.addElement(s);
				validateEvent(eventsListModel.getSize() - 1);
			}
		} finally {
			performCallbacks = tempPerformCallbacks;
		}

		commandSetSelection = null;
		setCommandSetControlsEnabled(false);
		commandSelection = -1;
		setCommandControlsEnabled(false);
		// toggle tabs to set button titles
		statesEventsTabs.setSelectedIndex(1);
		statesEventsTabs.setSelectedIndex(0);

		treeValid = isAnimationTreeValid();
		treeInvalidLabel.setVisible(!treeValid);

		constructCharacterInstance();
	}

	@Override
	public void deselect() {}

	@Override
	public void close() {}

	private void validateState(String name) {
		for (int i = 0; i < statesListModel.getSize(); ++i) {
			if (statesListModel.get(i).equals(name)) {
				validateState(i);
				break;
			}
		}
	}

	private void validateState(int index) {
		AnimationStateMachine.CommandSet cs = editor.animationStateMachine.getState(statesListModel.get(index));
		boolean valid = isCommandSetValid(cs);
		statesListRenderer.set(index, valid ? ACTIVE_COLOR : ERROR_COLOR);
		// trigger repaint
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			statesListModel.set(index, statesListModel.get(index));
		} finally {
			performCallbacks = tempPerformCallbacks;
		}
	}

	private void validateAllStatesAndEvents() {
		for (int i = 0; i < statesListModel.getSize(); ++i) {
			validateState(i);
		}
		for (int i = 0; i < eventsListModel.getSize(); ++i) {
			validateEvent(i);
		}
	}

	private void validateEvent(String name) {
		for (int i = 0; i < eventsListModel.getSize(); ++i) {
			if (eventsListModel.get(i).equals(name)) {
				validateEvent(i);
				break;
			}
		}
	}

	private void validateEvent(int index) {
		AnimationStateMachine.CommandSet cs = editor.animationStateMachine.getEvent(eventsListModel.get(index));
		boolean valid = isCommandSetValid(cs);
		eventsListRenderer.set(index, valid ? ACTIVE_COLOR : ERROR_COLOR);
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			eventsListModel.set(index, eventsListModel.get(index));
		} finally {
			performCallbacks = tempPerformCallbacks;
		}
	}

	private void validateCommand(int index) {
		AnimationStateMachine.CommandSet cs = (modeSelection == 0) ?
				editor.animationStateMachine.getState(commandSetSelection) :
				editor.animationStateMachine.getEvent(commandSetSelection);
		AnimationStateMachine.Command command = cs.getCommand(index);
		boolean valid = isCommandValid(command);
		commandsListRenderer.set(index, valid ? ACTIVE_COLOR : ERROR_COLOR);
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			commandsListModel.set(index, commandsListModel.get(index));
		} finally {
			performCallbacks = tempPerformCallbacks;
		}
	}

	private int modeSelection;          // 0 = states, 1 = events
	private String commandSetSelection; // name of selected command set within either states or events
	private int commandSelection;       // ID of selected command

	private void storeCommandSetProperties() {
		if (commandSetSelection == null) {
			return;
		}
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			// name
			String name = nameField.getText();
			if (name.isEmpty() ||
					(modeSelection == 0 && editor.animationStateMachine.getState(name) != null) ||
					(modeSelection == 1 && editor.animationStateMachine.getEvent(name) != null)) {
				// revert to current command set name
				nameField.setText(commandSetSelection);
			} else {
				// set the command set's name
				String oldName = commandSetSelection;
				DefaultListModel<String> model;
				if (modeSelection == 0) {
					editor.animationStateMachine.setStateName(oldName, name);
					model = statesListModel;
				} else {
					assert(modeSelection == 1);
					editor.animationStateMachine.setEventName(oldName, name);
					model = eventsListModel;
				}
				for (int i = 0; i < model.size(); ++i) {
					if (model.get(i).equals(oldName)) {
						model.set(i, name);
						break;
					}
				}
				commandSetSelection = name;
			}
		} finally {
			performCallbacks = tempPerformCallbacks;
		}

		constructCharacterInstance();
	}

	private void loadCommandSetProperties() {
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			nameField.setText(commandSetSelection);

			AnimationStateMachine.CommandSet cs = (modeSelection == 0) ?
					editor.animationStateMachine.getState(commandSetSelection) :
					editor.animationStateMachine.getEvent(commandSetSelection);
			// set up the commands
			commandsListRenderer.clear();
			commandsListModel.clear();
			for (int i = 0; i < cs.getCommandCount(); ++i) {
				commandsListRenderer.add(commandsListModel.getSize(), ACTIVE_COLOR);
				commandsListModel.addElement(cs.getCommand(i).getType().toString());
				validateCommand(commandsListModel.getSize()-1);
			}
		} finally {
			performCallbacks = tempPerformCallbacks;
		}
	}

	private void setCommandSetControlsEnabled(boolean e) {
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			nameField.setEnabled(e);
			removeButton.setEnabled(e);
			commandsList.setEnabled(e);
			addCommandButton.setEnabled(e);
		} finally {
			performCallbacks = tempPerformCallbacks;
		}
	}

	private AnimationStateMachine.FloatBinding storeFloatBindingControls(JSpinner constantSpinner, JTextField variableField, JToggleButton bindButton) {
		return bindButton.isSelected() ?
				new AnimationStateMachine.FloatBinding(variableField.getText()) :
				new AnimationStateMachine.FloatBinding((Float) constantSpinner.getValue());
	}

	private AnimationStateMachine.IntBinding storeIntBindingControls(JSpinner constantSpinner, JTextField variableField, JToggleButton bindButton) {
		return bindButton.isSelected() ?
				new AnimationStateMachine.IntBinding(variableField.getText()) :
				new AnimationStateMachine.IntBinding((Integer) constantSpinner.getValue());
	}

	private void storeCommandProperties() {
		if (commandSelection < 0) {
			return;
		}
		boolean validateAll = false;
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			AnimationStateMachine.CommandSet commandSet = (modeSelection == 0) ?
					editor.animationStateMachine.getState(commandSetSelection) :
					editor.animationStateMachine.getEvent(commandSetSelection);

			// store specific type - we should never have to change type in this function
			AnimationStateMachine.Command command = commandSet.getCommand(commandSelection);
			assert(command.getType().toString().equals(typeField.getText()));

			switch (command.getType()) {
				case ANIMATION_PLAY:
				{
					AnimationStateMachine.AnimationPlayCommand c = (AnimationStateMachine.AnimationPlayCommand) command;
					c.setNodeName(animationPlayNodeNameField.getText());
					validateNodeField(animationPlayNodeNameField, AnimationTree.Node.Type.ANIMATION);
					c.setLoop(animationPlayLoopCheckbox.isSelected());
					c.setSpeed(storeFloatBindingControls(
							animationPlaySpeedConstantSpinner,
							animationPlaySpeedVariableField,
							animationPlaySpeedVarButton));
					break;
				}
				case ANIMATION_SET_TIME:
				{
					AnimationStateMachine.AnimationSetTimeCommand c = (AnimationStateMachine.AnimationSetTimeCommand) command;
					c.setNodeName(animationSetTimeNodeNameField.getText());
					validateNodeField(animationSetTimeNodeNameField, AnimationTree.Node.Type.ANIMATION);
					c.setTime(storeFloatBindingControls(
							animationSetTimeTimeConstantSpinner,
							animationSetTimeTimeVariableField,
							animationSetTimeTimeVarButton));
					break;
				}
				case SELECT_SET_INDEX:
				{
					AnimationStateMachine.SelectSetIndexCommand c = (AnimationStateMachine.SelectSetIndexCommand) command;
					c.setNodeName(selectSetIndexNodeNameField.getText());
					validateNodeField(selectSetIndexNodeNameField, AnimationTree.Node.Type.SELECT);
					c.setIndex(storeIntBindingControls(
							selectSetIndexIndexConstantSpinner,
							selectSetIndexIndexVariableField,
							selectSetIndexIndexVarButton));
					break;
				}
				case BLEND_TRANSITION_WEIGHT:
				{
					AnimationStateMachine.BlendTransitionWeightCommand c = (AnimationStateMachine
							.BlendTransitionWeightCommand) command;
					c.setNodeName(blendTransitionWeightNodeNameField.getText());
					validateNodeField(blendTransitionWeightNodeNameField, AnimationTree.Node.Type.BLEND);
					c.setWeight(storeFloatBindingControls(
							blendTransitionWeightWeightConstantSpinner,
							blendTransitionWeightWeightVariableField,
							blendTransitionWeightWeightVarButton));
					c.setSpeed(storeFloatBindingControls(
							blendTransitionWeightSpeedConstantSpinner,
							blendTransitionWeightSpeedVariableField,
							blendTransitionWeightSpeedVarButton));
					break;
				}
				case BLEND_SET_WEIGHT:
				{
					AnimationStateMachine.BlendSetWeightCommand c = (AnimationStateMachine.BlendSetWeightCommand) command;
					c.setNodeName(blendSetWeightNodeNameField.getText());
					validateNodeField(blendSetWeightNodeNameField, AnimationTree.Node.Type.BLEND);
					c.setWeight(storeFloatBindingControls(
							blendSetWeightWeightConstantSpinner,
							blendSetWeightWeightVariableField,
							blendSetWeightWeightVarButton));
					break;
				}
				case SEQUENCE_TRANSITION_WEIGHT:
				{
					AnimationStateMachine.SequenceTransitionWeightCommand c = (AnimationStateMachine
							.SequenceTransitionWeightCommand) command;
					c.setNodeName(sequenceTransitionWeightNodeNameField.getText());
					validateNodeField(sequenceTransitionWeightNodeNameField, AnimationTree.Node.Type.SEQUENCE);
					c.setIndex((Integer) sequenceTransitionWeightIndexSpinner.getValue());
					c.setWeight(storeFloatBindingControls(
							sequenceTransitionWeightWeightConstantSpinner,
							sequenceTransitionWeightWeightVariableField,
							sequenceTransitionWeightWeightVarButton));
					c.setSpeed(storeFloatBindingControls(
							sequenceTransitionWeightSpeedConstantSpinner,
							sequenceTransitionWeightSpeedVariableField,
							sequenceTransitionWeightSpeedVarButton));
					break;
				}
				case SEQUENCE_SET_WEIGHT:
				{
					AnimationStateMachine.SequenceSetWeightCommand c = (AnimationStateMachine.SequenceSetWeightCommand) command;
					c.setNodeName(sequenceSetWeightNodeNameField.getText());
					validateNodeField(sequenceSetWeightNodeNameField, AnimationTree.Node.Type.SEQUENCE);
					c.setIndex((Integer) sequenceSetWeightIndexSpinner.getValue());
					c.setWeight(storeFloatBindingControls(
							sequenceSetWeightWeightConstantSpinner,
							sequenceSetWeightWeightVariableField,
							sequenceSetWeightWeightVarButton));
					break;
				}
				case STATE_SET_ACTIVE:
				{
					AnimationStateMachine.StateSetActiveCommand c = (AnimationStateMachine.StateSetActiveCommand) command;
					c.setStateName(stateSetActiveStateNameField.getText());
					validateStateField(stateSetActiveStateNameField);
					c.setActive(stateSetActiveActiveCheckbox.isSelected());
					break;
				}
				case EVENT_TRIGGER:
				{
					AnimationStateMachine.EventTriggerCommand c = (AnimationStateMachine.EventTriggerCommand) command;
					c.setEventName(eventTriggerEventNameField.getText());
					validateEventField(eventTriggerEventNameField);
					validateAll = true;
					break;
				}
				default:
					throw new IllegalArgumentException("Unknown command type " + command.getType());
			}

			validateCommand(commandSelection);
			if (modeSelection == 0) {
				validateState(commandSetSelection);
			} else {
				validateEvent(commandSetSelection);
			}
			if (validateAll) {
				validateAllStatesAndEvents();
			}
		} finally {
			performCallbacks = tempPerformCallbacks;
		}

		constructCharacterInstance();
	}

	private void validateNodeField(JTextField field, AnimationTree.Node.Type type) {
		String nodeName = field.getText();
		AnimationTree.Node node = editor.animationTree.getNode(nodeName);
		boolean valid = (node != null) && (node.getType() == type);
		field.setBackground(valid ? NORMAL_FIELD_COLOR : ERROR_FIELD_COLOR);
	}

	private void validateStateField(JTextField field) {
		String stateName = field.getText();
		AnimationStateMachine.CommandSet cs = editor.animationStateMachine.getState(stateName);
		boolean valid = cs != null;
		field.setBackground(valid ? NORMAL_FIELD_COLOR : ERROR_FIELD_COLOR);
	}

	private void validateEventField(JTextField field) {
		String eventName = field.getText();
		AnimationStateMachine.CommandSet cs = editor.animationStateMachine.getEvent(eventName);
		boolean valid = (cs != null) && !isEventCyclic(eventName);
		field.setBackground(valid ? NORMAL_FIELD_COLOR : ERROR_FIELD_COLOR);
	}

	private void loadBindingControls(JSpinner constantSpinner, JTextField variableField, JToggleButton bindButton,
	                                 AnimationStateMachine.FloatBinding binding) {
		bindButton.setSelected(binding.isBoundToVariable());
		constantSpinner.setVisible(!binding.isBoundToVariable());
		variableField.setVisible(binding.isBoundToVariable());
		if (binding.isBoundToVariable()) {
			variableField.setText(binding.getVariable());
		} else {
			constantSpinner.setValue(binding.getConstant());
		}
	}

	private void loadBindingControls(JSpinner constantSpinner, JTextField variableField, JToggleButton bindButton,
	                                 AnimationStateMachine.IntBinding binding) {
		bindButton.setSelected(binding.isBoundToVariable());
		constantSpinner.setVisible(!binding.isBoundToVariable());
		variableField.setVisible(binding.isBoundToVariable());
		if (binding.isBoundToVariable()) {
			variableField.setText(binding.getVariable());
		} else {
			constantSpinner.setValue(binding.getConstant());
		}
	}

	private void loadCommandProperties() {
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			AnimationStateMachine.CommandSet commandSet = (modeSelection == 0) ?
					editor.animationStateMachine.getState(commandSetSelection) :
					editor.animationStateMachine.getEvent(commandSetSelection);
			AnimationStateMachine.Command command = commandSet.getCommand(commandSelection);

			typeField.setText(command.getType().toString());

			switch (command.getType()) {
				case ANIMATION_PLAY:
				{
					AnimationStateMachine.AnimationPlayCommand c = (AnimationStateMachine.AnimationPlayCommand) command;
					animationPlayNodeNameField.setText(c.getNodeName());
					validateNodeField(animationPlayNodeNameField, AnimationTree.Node.Type.ANIMATION);
					animationPlayLoopCheckbox.setSelected(c.getLoop());
					loadBindingControls(
							animationPlaySpeedConstantSpinner,
							animationPlaySpeedVariableField,
							animationPlaySpeedVarButton,
							c.getSpeed());
					break;
				}
				case ANIMATION_SET_TIME:
				{
					AnimationStateMachine.AnimationSetTimeCommand c = (AnimationStateMachine.AnimationSetTimeCommand) command;
					animationSetTimeNodeNameField.setText(c.getNodeName());
					validateNodeField(animationSetTimeNodeNameField, AnimationTree.Node.Type.ANIMATION);
					loadBindingControls(
							animationSetTimeTimeConstantSpinner,
							animationSetTimeTimeVariableField,
							animationSetTimeTimeVarButton,
							c.getTime());
					break;
				}
				case SELECT_SET_INDEX:
				{
					AnimationStateMachine.SelectSetIndexCommand c = (AnimationStateMachine.SelectSetIndexCommand) command;
					selectSetIndexNodeNameField.setText(c.getNodeName());
					validateNodeField(selectSetIndexNodeNameField, AnimationTree.Node.Type.SELECT);
					loadBindingControls(
							selectSetIndexIndexConstantSpinner,
							selectSetIndexIndexVariableField,
							selectSetIndexIndexVarButton,
							c.getIndex());
					break;
				}
				case BLEND_TRANSITION_WEIGHT:
				{
					AnimationStateMachine.BlendTransitionWeightCommand c = (AnimationStateMachine
							.BlendTransitionWeightCommand) command;
					blendTransitionWeightNodeNameField.setText(c.getNodeName());
					validateNodeField(blendTransitionWeightNodeNameField, AnimationTree.Node.Type.BLEND);
					loadBindingControls(
							blendTransitionWeightWeightConstantSpinner,
							blendTransitionWeightWeightVariableField,
							blendTransitionWeightWeightVarButton,
							c.getWeight());
					loadBindingControls(
							blendTransitionWeightSpeedConstantSpinner,
							blendTransitionWeightSpeedVariableField,
							blendTransitionWeightSpeedVarButton,
							c.getSpeed());
					break;
				}
				case BLEND_SET_WEIGHT:
				{
					AnimationStateMachine.BlendSetWeightCommand c = (AnimationStateMachine.BlendSetWeightCommand) command;
					blendSetWeightNodeNameField.setText(c.getNodeName());
					validateNodeField(blendSetWeightNodeNameField, AnimationTree.Node.Type.BLEND);
					loadBindingControls(
							blendSetWeightWeightConstantSpinner,
							blendSetWeightWeightVariableField,
							blendSetWeightWeightVarButton,
							c.getWeight());
					break;
				}
				case SEQUENCE_TRANSITION_WEIGHT:
				{
					AnimationStateMachine.SequenceTransitionWeightCommand c = (AnimationStateMachine
							.SequenceTransitionWeightCommand) command;
					sequenceTransitionWeightNodeNameField.setText(c.getNodeName());
					validateNodeField(sequenceTransitionWeightNodeNameField, AnimationTree.Node.Type.SEQUENCE);
					sequenceTransitionWeightIndexSpinner.setValue(c.getIndex());
					loadBindingControls(
							sequenceTransitionWeightWeightConstantSpinner,
							sequenceTransitionWeightWeightVariableField,
							sequenceTransitionWeightWeightVarButton,
							c.getWeight());
					loadBindingControls(
							sequenceTransitionWeightSpeedConstantSpinner,
							sequenceTransitionWeightSpeedVariableField,
							sequenceTransitionWeightSpeedVarButton,
							c.getSpeed());
					break;
				}
				case SEQUENCE_SET_WEIGHT:
				{
					AnimationStateMachine.SequenceSetWeightCommand c = (AnimationStateMachine.SequenceSetWeightCommand) command;
					sequenceSetWeightNodeNameField.setText(c.getNodeName());
					validateNodeField(sequenceSetWeightNodeNameField, AnimationTree.Node.Type.SEQUENCE);
					sequenceSetWeightIndexSpinner.setValue(c.getIndex());
					loadBindingControls(
							sequenceSetWeightWeightConstantSpinner,
							sequenceSetWeightWeightVariableField,
							sequenceSetWeightWeightVarButton,
							c.getWeight());
					break;
				}
				case STATE_SET_ACTIVE:
				{
					AnimationStateMachine.StateSetActiveCommand c = (AnimationStateMachine.StateSetActiveCommand) command;
					stateSetActiveStateNameField.setText(c.getStateName());
					validateStateField(stateSetActiveStateNameField);
					stateSetActiveActiveCheckbox.setSelected(c.getActive());
					break;
				}
				case EVENT_TRIGGER:
				{
					AnimationStateMachine.EventTriggerCommand c = (AnimationStateMachine.EventTriggerCommand) command;
					eventTriggerEventNameField.setText(c.getEventName());
					validateEventField(eventTriggerEventNameField);
					break;
				}
				default:
					throw new IllegalArgumentException("Unknown command type " + command.getType());
			}
		} finally {
			performCallbacks = tempPerformCallbacks;
		}
	}

	private void setCommandControlsEnabled(boolean e) {
		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			typeField.setEnabled(e);
			removeCommandButton.setEnabled(e);

			// disable all controls by default - selectively enable
			boolean animationPlayEnabled = false;
			boolean animationSetTimeEnabled = false;
			boolean selectSetIndexEnabled = false;
			boolean blendTransitionWeightEnabled = false;
			boolean blendSetWeightEnabled = false;
			boolean sequenceTransitionWeightEnabled = false;
			boolean sequenceSetWeightEnabled = false;
			boolean stateSetActiveEnabled = false;
			boolean eventTriggerEnabled = false;

			if (e) {
				AnimationStateMachine.CommandSet commandSet = (modeSelection == 0) ?
						editor.animationStateMachine.getState(commandSetSelection) :
						editor.animationStateMachine.getEvent(commandSetSelection);
				AnimationStateMachine.Command command = commandSet.getCommand(commandSelection);

				// selectively enable controls based on type
				switch (command.getType()) {
					case ANIMATION_PLAY:
						animationPlayEnabled = true;
						break;
					case ANIMATION_SET_TIME:
						animationSetTimeEnabled = true;
						break;
					case SELECT_SET_INDEX:
						selectSetIndexEnabled = true;
						break;
					case BLEND_TRANSITION_WEIGHT:
						blendTransitionWeightEnabled = true;
						break;
					case BLEND_SET_WEIGHT:
						blendSetWeightEnabled = true;
						break;
					case SEQUENCE_TRANSITION_WEIGHT:
						sequenceTransitionWeightEnabled = true;
						break;
					case SEQUENCE_SET_WEIGHT:
						sequenceSetWeightEnabled = true;
						break;
					case STATE_SET_ACTIVE:
						stateSetActiveEnabled = true;
						break;
					case EVENT_TRIGGER:
						eventTriggerEnabled = true;
						break;
					default:
						throw new IllegalArgumentException("Invalid command type " + command.getType());
				}
			}

			animationPlayNodeNameLabel.setVisible(animationPlayEnabled);
			animationPlayNodeNameField.setVisible(animationPlayEnabled);
			animationPlayLoopCheckbox.setVisible(animationPlayEnabled);
			animationPlaySpeedLabel.setVisible(animationPlayEnabled);
			animationPlaySpeedConstantSpinner.setVisible(animationPlayEnabled);
			animationPlaySpeedVariableField.setVisible(animationPlayEnabled);
			animationPlaySpeedVarButton.setVisible(animationPlayEnabled);

			animationSetTimeNodeNameLabel.setVisible(animationSetTimeEnabled);
			animationSetTimeNodeNameField.setVisible(animationSetTimeEnabled);
			animationSetTimeTimeLabel.setVisible(animationSetTimeEnabled);
			animationSetTimeTimeConstantSpinner.setVisible(animationSetTimeEnabled);
			animationSetTimeTimeVariableField.setVisible(animationSetTimeEnabled);
			animationSetTimeTimeVarButton.setVisible(animationSetTimeEnabled);

			selectSetIndexNodeNameLabel.setVisible(selectSetIndexEnabled);
			selectSetIndexNodeNameField.setVisible(selectSetIndexEnabled);
			selectSetIndexIndexLabel.setVisible(selectSetIndexEnabled);
			selectSetIndexIndexConstantSpinner.setVisible(selectSetIndexEnabled);
			selectSetIndexIndexVariableField.setVisible(selectSetIndexEnabled);
			selectSetIndexIndexVarButton.setVisible(selectSetIndexEnabled);

			blendTransitionWeightNodeNameLabel.setVisible(blendTransitionWeightEnabled);
			blendTransitionWeightNodeNameField.setVisible(blendTransitionWeightEnabled);
			blendTransitionWeightWeightLabel.setVisible(blendTransitionWeightEnabled);
			blendTransitionWeightWeightConstantSpinner.setVisible(blendTransitionWeightEnabled);
			blendTransitionWeightWeightVariableField.setVisible(blendTransitionWeightEnabled);
			blendTransitionWeightWeightVarButton.setVisible(blendTransitionWeightEnabled);
			blendTransitionWeightSpeedLabel.setVisible(blendTransitionWeightEnabled);
			blendTransitionWeightSpeedConstantSpinner.setVisible(blendTransitionWeightEnabled);
			blendTransitionWeightSpeedVariableField.setVisible(blendTransitionWeightEnabled);
			blendTransitionWeightSpeedVarButton.setVisible(blendTransitionWeightEnabled);

			blendSetWeightNodeNameLabel.setVisible(blendSetWeightEnabled);
			blendSetWeightNodeNameField.setVisible(blendSetWeightEnabled);
			blendSetWeightWeightLabel.setVisible(blendSetWeightEnabled);
			blendSetWeightWeightConstantSpinner.setVisible(blendSetWeightEnabled);
			blendSetWeightWeightVariableField.setVisible(blendSetWeightEnabled);
			blendSetWeightWeightVarButton.setVisible(blendSetWeightEnabled);

			sequenceTransitionWeightNodeNameLabel.setVisible(sequenceTransitionWeightEnabled);
			sequenceTransitionWeightNodeNameField.setVisible(sequenceTransitionWeightEnabled);
			sequenceTransitionWeightIndexLabel.setVisible(sequenceTransitionWeightEnabled);
			sequenceTransitionWeightIndexSpinner.setVisible(sequenceTransitionWeightEnabled);
			sequenceTransitionWeightWeightLabel.setVisible(sequenceTransitionWeightEnabled);
			sequenceTransitionWeightWeightConstantSpinner.setVisible(sequenceTransitionWeightEnabled);
			sequenceTransitionWeightWeightVariableField.setVisible(sequenceTransitionWeightEnabled);
			sequenceTransitionWeightWeightVarButton.setVisible(sequenceTransitionWeightEnabled);
			sequenceTransitionWeightSpeedLabel.setVisible(sequenceTransitionWeightEnabled);
			sequenceTransitionWeightSpeedConstantSpinner.setVisible(sequenceTransitionWeightEnabled);
			sequenceTransitionWeightSpeedVariableField.setVisible(sequenceTransitionWeightEnabled);
			sequenceTransitionWeightSpeedVarButton.setVisible(sequenceTransitionWeightEnabled);

			sequenceSetWeightNodeNameLabel.setVisible(sequenceSetWeightEnabled);
			sequenceSetWeightNodeNameField.setVisible(sequenceSetWeightEnabled);
			sequenceSetWeightIndexLabel.setVisible(sequenceSetWeightEnabled);
			sequenceSetWeightIndexSpinner.setVisible(sequenceSetWeightEnabled);
			sequenceSetWeightWeightLabel.setVisible(sequenceSetWeightEnabled);
			sequenceSetWeightWeightConstantSpinner.setVisible(sequenceSetWeightEnabled);
			sequenceSetWeightWeightVariableField.setVisible(sequenceSetWeightEnabled);
			sequenceSetWeightWeightVarButton.setVisible(sequenceSetWeightEnabled);

			stateSetActiveStateNameLabel.setVisible(stateSetActiveEnabled);
			stateSetActiveStateNameField.setVisible(stateSetActiveEnabled);
			stateSetActiveActiveCheckbox.setVisible(stateSetActiveEnabled);

			eventTriggerEventNameLabel.setVisible(eventTriggerEnabled);
			eventTriggerEventNameField.setVisible(eventTriggerEnabled);
		} finally {
			performCallbacks = tempPerformCallbacks;
		}
	}

	private void setCommandType(AnimationStateMachine.Command.Type type) {
		AnimationStateMachine.CommandSet commandSet = (modeSelection == 0) ?
				editor.animationStateMachine.getState(commandSetSelection) :
				editor.animationStateMachine.getEvent(commandSetSelection);

		AnimationStateMachine.Command oldCommand = commandSet.getCommand(commandSelection);
		if (oldCommand.getType() == type) {
			return; // nothing to change
		}
		commandSet.removeCommand(commandSelection);
		AnimationStateMachine.Command newCommand;

		switch (type) {
			case ANIMATION_PLAY:
			{
				AnimationStateMachine.AnimationPlayCommand c = new AnimationStateMachine.AnimationPlayCommand();
				c.setNodeName("");
				newCommand = c;
				break;
			}
			case ANIMATION_SET_TIME:
			{
				AnimationStateMachine.AnimationSetTimeCommand c = new AnimationStateMachine.AnimationSetTimeCommand();
				c.setNodeName("");
				newCommand = c;
				break;
			}
			case SELECT_SET_INDEX:
			{
				AnimationStateMachine.SelectSetIndexCommand c = new AnimationStateMachine.SelectSetIndexCommand();
				c.setNodeName("");
				newCommand = c;
				break;
			}
			case BLEND_TRANSITION_WEIGHT:
			{
				AnimationStateMachine.BlendTransitionWeightCommand c = new AnimationStateMachine.BlendTransitionWeightCommand();
				c.setNodeName("");
				newCommand = c;
				break;
			}
			case BLEND_SET_WEIGHT:
			{
				AnimationStateMachine.BlendSetWeightCommand c = new AnimationStateMachine.BlendSetWeightCommand();
				c.setNodeName("");
				newCommand = c;
				break;
			}
			case SEQUENCE_TRANSITION_WEIGHT:
			{
				AnimationStateMachine.SequenceTransitionWeightCommand c = new AnimationStateMachine
						.SequenceTransitionWeightCommand();
				c.setNodeName("");
				newCommand = c;
				break;
			}
			case SEQUENCE_SET_WEIGHT:
			{
				AnimationStateMachine.SequenceSetWeightCommand c = new AnimationStateMachine.SequenceSetWeightCommand();
				c.setNodeName("");
				newCommand = c;
				break;
			}
			case STATE_SET_ACTIVE:
			{
				AnimationStateMachine.StateSetActiveCommand c = new AnimationStateMachine.StateSetActiveCommand();
				c.setStateName("");
				newCommand = c;
				break;
			}
			case EVENT_TRIGGER:
			{
				AnimationStateMachine.EventTriggerCommand c = new AnimationStateMachine.EventTriggerCommand();
				c.setEventName("");
				newCommand = c;
				break;
			}
			default:
				throw new IllegalArgumentException("Unknown command type " + type);
		}

		commandSet.addCommand(commandSelection, newCommand);

		// change the display name in the list
		commandsListModel.set(commandSelection, type.toString());
		validateCommand(commandSelection);
		if (modeSelection == 0) {
			validateState(commandSetSelection);
		} else {
			validateEvent(commandSetSelection);
		}

		constructCharacterInstance();

		// refresh GUI
		setCommandControlsEnabled(true);
		loadCommandProperties();
	}

	private void addCommandSet() {
		int addSelection = statesEventsTabs.getSelectedIndex();

		JList<String> list = (addSelection == 0) ? statesList : eventsList;
		DefaultListModel<String> model = (addSelection == 0) ? statesListModel : eventsListModel;
		ColoredListRenderer renderer = (addSelection == 0) ? statesListRenderer : eventsListRenderer;
		String prefix = (addSelection == 0) ? "state" : "event";

		int val = 0;
		// find unused node name
		boolean used = true;
		while (used) {
			if ((addSelection == 0) ?
					editor.animationStateMachine.getState(prefix + val) == null :
					editor.animationStateMachine.getEvent(prefix + val) == null) {
				used = false;
			} else {
				++val;
			}
		}

		String name = prefix + val;

		if (addSelection == 0) {
			editor.animationStateMachine.addState(name);
		} else {
			assert(addSelection == 1);
			editor.animationStateMachine.addEvent(name);
		}

		renderer.add(model.getSize(), ACTIVE_COLOR);
		model.add(model.getSize(), name);
		if (addSelection == 0) {
			validateState(commandSetSelection);
		} else {
			validateEvent(commandSetSelection);
		}
		constructCharacterInstance();
		list.setSelectedIndex(model.getSize()-1);
	}

	private void removeCommandSet() {
		String sel = commandSetSelection;

		JList<String> list = (modeSelection == 0) ? statesList : eventsList;
		DefaultListModel<String> model = (modeSelection == 0) ? statesListModel : eventsListModel;
		ColoredListRenderer renderer = (modeSelection == 0) ? statesListRenderer : eventsListRenderer;

		list.clearSelection();

		if (modeSelection == 0) {
			editor.animationStateMachine.removeState(sel);
		} else {
			assert(modeSelection == 1);
			editor.animationStateMachine.removeEvent(sel);
		}

		for (int i = 0; i < model.getSize(); ++i) {
			if (model.get(i).equals(sel)) {
				renderer.remove(i);
				model.remove(i);
				break;
			}
		}

		constructCharacterInstance();
	}

	private void addCommand() {
		AnimationStateMachine.CommandSet cs = (modeSelection == 0) ?
				editor.animationStateMachine.getState(commandSetSelection) :
				editor.animationStateMachine.getEvent(commandSetSelection);

		// add an ANIMATION_PLAY command by default
		AnimationStateMachine.AnimationPlayCommand cmd = new AnimationStateMachine.AnimationPlayCommand();
		cmd.setNodeName("");

		int index = (commandSelection < 0) ?
				commandsListModel.getSize() : commandsList.getSelectedIndex();
		if (index <= commandSelection) {
			++commandSelection; // we're inserting before, so the current selection index is incremented
		}

		cs.addCommand(index, cmd);

		commandsListRenderer.add(index, ACTIVE_COLOR);
		commandsListModel.add(index, cmd.getType().toString());
		validateCommand(index);
		if (modeSelection == 0) {
			validateState(commandSetSelection);
		} else {
			validateEvent(commandSetSelection);
		}
		constructCharacterInstance();
		commandsList.setSelectedIndex(index);
	}

	private void removeCommand() {
		int sel = commandSelection;

		AnimationStateMachine.CommandSet cs = (modeSelection == 0) ?
				editor.animationStateMachine.getState(commandSetSelection) :
				editor.animationStateMachine.getEvent(commandSetSelection);

		commandsList.clearSelection();

		cs.removeCommand(sel);
		commandsListModel.remove(sel);
		commandsListRenderer.remove(sel);
		if (modeSelection == 0) {
			validateState(commandSetSelection);
		} else {
			validateEvent(commandSetSelection);
		}
		constructCharacterInstance();
	}

	private void moveCommand(int oldIndex, int newIndex) {
		AnimationStateMachine.CommandSet cs = (modeSelection == 0) ?
				editor.animationStateMachine.getState(commandSetSelection) :
				editor.animationStateMachine.getEvent(commandSetSelection);
		AnimationStateMachine.Command cmd = cs.getCommand(oldIndex);

		if (oldIndex < newIndex) {
			// if the source index is less than the destination index, decrement the destination index by one
			--newIndex;
		}
		cs.removeCommand(oldIndex);
		cs.addCommand(newIndex, cmd);

		boolean tempPerformCallbacks = performCallbacks;
		performCallbacks = false;
		try {
			Color c = commandsListRenderer.get(oldIndex);
			commandsListRenderer.remove(oldIndex);
			commandsListModel.remove(oldIndex);
			commandsListRenderer.add(newIndex, c);
			commandsListModel.add(newIndex, cmd.getType().toString());
			commandSelection = newIndex;
			commandsList.setSelectedIndex(newIndex);
		} finally {
			performCallbacks = tempPerformCallbacks;
		}

		constructCharacterInstance();
	}

	public void constructCharacterInstance() {
		// validate each command set
		stateMachineValid = true;
		if (treeValid) {
			for (String s : editor.animationStateMachine.getStates()) {
				if (!isCommandSetValid(editor.animationStateMachine.getState(s))) {
					stateMachineValid = false;
					break;
				}
			}
			if (stateMachineValid) {
				for (String s : editor.animationStateMachine.getEvents()) {
					if (!isCommandSetValid(editor.animationStateMachine.getEvent(s))) {
						stateMachineValid = false;
						break;
					}
				}
			}
		}
		if (!treeValid || !stateMachineValid) {
			// default character if either part is invalid
			editor.character = new Character(
					editor.skeleton,
					new AnimationTree(),            // empty tree
					new AnimationStateMachine(),    // empty state machine
					new HashMap<String, Animation>());
		} else {
			HashMap<String, Animation> animations = new HashMap<String, Animation>();
			for (int i = 0; i < editor.animations.size(); ++i) {
				animations.put(editor.animationNames.get(i), editor.animations.get(i));
			}
			editor.character = new Character(
					editor.skeleton,
					editor.animationTree,
					editor.animationStateMachine,
					animations);
		}
		editor.state = editor.character.createCharacterState();
		editor.evaluator.evaluate(editor.state);

		editor.boneColors = new Vector4f[editor.skeleton.getBoneCount()];

		final Vector4f WHITE = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f);
		for (int i = 0; i < editor.boneColors.length; ++i) {
			editor.boneColors[i] = WHITE;
		}

		rebuildVariables();
		setStateActiveColors();

		if (treeValid && stateMachineValid) {
			for (Map.Entry<String, Float> e : floatVariables.entrySet()) {
				editor.state.setVariable(e.getKey(), e.getValue());
			}
			for (Map.Entry<String, Integer> e : intVariables.entrySet()) {
				editor.state.setVariable(e.getKey(), e.getValue());
			}
		}
	}

	private boolean isAnimationTreeValid() {
		return isNodeValid(editor.animationTree.getRoot());
	}

	private boolean isNodeValid(AnimationTree.Node node) {
		if (node.getType() == AnimationTree.Node.Type.ANIMATION) {
			return editor.animationNames.contains(((AnimationTree.AnimationNode) node).getAnimation());
		} else {
			for (int i = 0; i < node.getChildCount(); ++i) {
				if (!isNodeValid(node.getChild(i))) {
					return false;
				}
			}
			return true;
		}
	}

	private boolean isCommandSetValid(AnimationStateMachine.CommandSet cs) {
		for (int c = 0; c < cs.getCommandCount(); ++c) {
			if (!isCommandValid(cs.getCommand(c))) {
				return false;
			}
		}
		return true;
	}

	private boolean isCommandValid(AnimationStateMachine.Command cmd) {
		switch (cmd.getType()) {
			case ANIMATION_PLAY:
			{
				AnimationStateMachine.AnimationPlayCommand c = (AnimationStateMachine.AnimationPlayCommand) cmd;
				AnimationTree.Node node = editor.animationTree.getNode(c.getNodeName());
				return node != null && node.getType() == AnimationTree.Node.Type.ANIMATION;
			}
			case ANIMATION_SET_TIME:
			{
				AnimationStateMachine.AnimationSetTimeCommand c = (AnimationStateMachine.AnimationSetTimeCommand) cmd;
				AnimationTree.Node node = editor.animationTree.getNode(c.getNodeName());
				return node != null && node.getType() == AnimationTree.Node.Type.ANIMATION;
			}
			case SELECT_SET_INDEX:
			{
				AnimationStateMachine.SelectSetIndexCommand c = (AnimationStateMachine.SelectSetIndexCommand) cmd;
				AnimationTree.Node node = editor.animationTree.getNode(c.getNodeName());
				return node != null && node.getType() == AnimationTree.Node.Type.SELECT;
			}
			case BLEND_TRANSITION_WEIGHT:
			{
				AnimationStateMachine.BlendTransitionWeightCommand c = (AnimationStateMachine
						.BlendTransitionWeightCommand) cmd;
				AnimationTree.Node node = editor.animationTree.getNode(c.getNodeName());
				return node != null && node.getType() == AnimationTree.Node.Type.BLEND;
			}
			case BLEND_SET_WEIGHT:
			{
				AnimationStateMachine.BlendSetWeightCommand c = (AnimationStateMachine.BlendSetWeightCommand) cmd;
				AnimationTree.Node node = editor.animationTree.getNode(c.getNodeName());
				return node != null && node.getType() == AnimationTree.Node.Type.BLEND;
			}
			case SEQUENCE_TRANSITION_WEIGHT:
			{
				AnimationStateMachine.SequenceTransitionWeightCommand c = (AnimationStateMachine
						.SequenceTransitionWeightCommand) cmd;
				AnimationTree.Node node = editor.animationTree.getNode(c.getNodeName());
				return node != null && node.getType() == AnimationTree.Node.Type.SEQUENCE;
			}
			case SEQUENCE_SET_WEIGHT:
			{
				AnimationStateMachine.SequenceSetWeightCommand c = (AnimationStateMachine.SequenceSetWeightCommand) cmd;
				AnimationTree.Node node = editor.animationTree.getNode(c.getNodeName());
				return node != null && node.getType() == AnimationTree.Node.Type.SEQUENCE;
			}
			case STATE_SET_ACTIVE:
			{
				AnimationStateMachine.StateSetActiveCommand c = (AnimationStateMachine.StateSetActiveCommand) cmd;
				AnimationStateMachine.CommandSet cs = editor.animationStateMachine.getState(c.getStateName());
				return cs != null;
			}
			case EVENT_TRIGGER:
			{
				AnimationStateMachine.EventTriggerCommand c = (AnimationStateMachine.EventTriggerCommand) cmd;
				AnimationStateMachine.CommandSet cs = editor.animationStateMachine.getEvent(c.getEventName());
				return cs != null && !isEventCyclic(c.getEventName());
			}
			default:
				throw new IllegalArgumentException("Invalid command type " + cmd.getType());
		}
	}

	private boolean isEventCyclic(String name) {
		HashSet<String> activeEvents = new HashSet<String>();
		return isEventCyclic(name, activeEvents);
	}

	private boolean isEventCyclic(String name, HashSet<String> activeEvents) {
		// check if the same event is being called from within itself - this will cause infinite loop
		if (activeEvents.contains(name)) {
			return true;
		}
		// add this event to the map so recursively called events can tell if it's active
		activeEvents.add(name);
		AnimationStateMachine.CommandSet cs = editor.animationStateMachine.getEvent(name);
		if (cs != null) {
			for (int i = 0; i < cs.getCommandCount(); ++i) {
				AnimationStateMachine.Command cmd = cs.getCommand(i);
				if (cmd.getType() == AnimationStateMachine.Command.Type.EVENT_TRIGGER) {
					if (isEventCyclic(((AnimationStateMachine.EventTriggerCommand) cmd).getEventName(), activeEvents)) {
						return true;
					}
				}
			}
		}
		// remove event from map - no longer active
		activeEvents.remove(name);
		return false;
	}

	private void rebuildVariables() {
		// remove all components
		variablesPanel.removeAll();

		GroupLayout layout = new GroupLayout(variablesPanel);
		variablesPanel.setLayout(layout);
		layout.setAutoCreateGaps(true);
		layout.setAutoCreateContainerGaps(true);

		HashMap<String, Float> oldFloatVariables = floatVariables;
		HashMap<String, Integer> oldIntVariables = intVariables;

		floatVariables = new HashMap<String, Float>();
		intVariables = new HashMap<String, Integer>();

		if (editor.animationStateMachine != null) {
			for (String s : editor.animationStateMachine.getStates()) {
				AnimationStateMachine.CommandSet cs = editor.animationStateMachine.getState(s);
				for (int i = 0; i < cs.getCommandCount(); ++i) {
					addCommandVariables(cs.getCommand(i), oldFloatVariables, oldIntVariables);
				}
			}
			for (String s : editor.animationStateMachine.getEvents()) {
				AnimationStateMachine.CommandSet cs = editor.animationStateMachine.getEvent(s);
				for (int i = 0; i < cs.getCommandCount(); ++i) {
					addCommandVariables(cs.getCommand(i), oldFloatVariables, oldIntVariables);
				}
			}
		}

		String[] sortedFloatVariables = new String[floatVariables.size()];
		floatVariables.keySet().toArray(sortedFloatVariables);
		String[] sortedIntVariables = new String[intVariables.size()];
		intVariables.keySet().toArray(sortedIntVariables);

		Arrays.sort(sortedFloatVariables);
		Arrays.sort(sortedIntVariables);

		GroupLayout.Group horizGroup = layout.createParallelGroup();
		GroupLayout.Group vertGroup = layout.createSequentialGroup();

		horizGroup.addComponent(floatVariablesLabel);
		vertGroup.addComponent(floatVariablesLabel);
		{
			JLabel[] floatLabels = new JLabel[sortedFloatVariables.length];
			JSpinner[] floatSpinners = new JSpinner[sortedFloatVariables.length];
			for (int i = 0; i < sortedFloatVariables.length; ++i) {
				floatLabels[i] = new JLabel(sortedFloatVariables[i] + ":");
				floatSpinners[i] = new JSpinner(new SpinnerNumberModel(
						floatVariables.get(sortedFloatVariables[i]),
						new Float(-100000.0f), new Float(100000.0f), new Float(1.0f)));
				final String name = sortedFloatVariables[i];
				floatSpinners[i].addChangeListener(new ChangeListener() {
					@Override
					public void stateChanged(ChangeEvent e) {
						synchronized (AnimationStateMachinePanel.this.editor) {
							Float value = (Float) ((JSpinner) e.getSource()).getValue();
							floatVariables.put(name, value);
							editor.state.setVariable(name, value);
						}
					}
				});
			}

			GroupLayout.Group labelsGroup = layout.createParallelGroup(GroupLayout.Alignment.TRAILING);
			for (int i = 0; i < sortedFloatVariables.length; ++i) {
				labelsGroup.addComponent(floatLabels[i]);
			}
			GroupLayout.Group spinnersGroup = layout.createParallelGroup(GroupLayout.Alignment.LEADING);
			for (int i = 0; i < sortedFloatVariables.length; ++i) {
				spinnersGroup.addComponent(floatSpinners[i]);
			}
			horizGroup.addGroup(layout.createSequentialGroup()
					.addGroup(labelsGroup)
					.addGroup(spinnersGroup));

			for (int i = 0; i < sortedFloatVariables.length; ++i) {
				vertGroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
						.addComponent(floatLabels[i])
						.addComponent(floatSpinners[i]));
			}
		}

		horizGroup.addComponent(variablesSeparator);
		horizGroup.addComponent(intVariablesLabel);
		vertGroup.addComponent(variablesSeparator, GroupLayout.PREFERRED_SIZE,
				GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE);
		vertGroup.addComponent(intVariablesLabel);
		{
			JLabel[] intLabels = new JLabel[sortedIntVariables.length];
			JSpinner[] intSpinners = new JSpinner[sortedIntVariables.length];
			for (int i = 0; i < sortedIntVariables.length; ++i) {
				intLabels[i] = new JLabel(sortedIntVariables[i] + ":");
				intSpinners[i] = new JSpinner(new SpinnerNumberModel(
						intVariables.get(sortedIntVariables[i]),
						new Integer(-100000), new Integer(100000), new Integer(1)));
				final String name = sortedIntVariables[i];
				intSpinners[i].addChangeListener(new ChangeListener() {
					@Override
					public void stateChanged(ChangeEvent e) {
						synchronized (AnimationStateMachinePanel.this.editor) {
							Integer value = (Integer) ((JSpinner) e.getSource()).getValue();
							intVariables.put(name, value);
							editor.state.setVariable(name, value);
						}
					}
				});
			}

			GroupLayout.Group labelsGroup = layout.createParallelGroup(GroupLayout.Alignment.TRAILING);
			for (int i = 0; i < sortedIntVariables.length; ++i) {
				labelsGroup.addComponent(intLabels[i]);
			}
			GroupLayout.Group spinnersGroup = layout.createParallelGroup(GroupLayout.Alignment.LEADING);
			for (int i = 0; i < sortedIntVariables.length; ++i) {
				spinnersGroup.addComponent(intSpinners[i]);
			}
			horizGroup.addGroup(layout.createSequentialGroup()
					.addGroup(labelsGroup)
					.addGroup(spinnersGroup));

			for (int i = 0; i < sortedIntVariables.length; ++i) {
				vertGroup.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
						.addComponent(intLabels[i])
						.addComponent(intSpinners[i]));
			}
		}

		layout.setHorizontalGroup(horizGroup);
		layout.setVerticalGroup(vertGroup);
	}

	private void addCommandVariables(AnimationStateMachine.Command command, HashMap<String, Float> oldFloatVariables, HashMap<String, Integer> oldIntVariables) {
		switch (command.getType()) {
			case ANIMATION_PLAY:
			{
				AnimationStateMachine.AnimationPlayCommand c = (AnimationStateMachine.AnimationPlayCommand) command;
				if (c.getSpeed().isBoundToVariable()) {
					addFloatVariable(c.getSpeed().getVariable(), oldFloatVariables);
				}
				break;
			}
			case ANIMATION_SET_TIME:
			{
				AnimationStateMachine.AnimationSetTimeCommand c = (AnimationStateMachine.AnimationSetTimeCommand) command;
				if (c.getTime().isBoundToVariable()) {
					addFloatVariable(c.getTime().getVariable(), oldFloatVariables);
				}
				break;
			}
			case SELECT_SET_INDEX:
			{
				AnimationStateMachine.SelectSetIndexCommand c = (AnimationStateMachine.SelectSetIndexCommand) command;
				if (c.getIndex().isBoundToVariable()) {
					addIntVariable(c.getIndex().getVariable(), oldIntVariables);
				}
				break;
			}
			case BLEND_TRANSITION_WEIGHT:
			{
				AnimationStateMachine.BlendTransitionWeightCommand c = (AnimationStateMachine
						.BlendTransitionWeightCommand) command;
				if (c.getWeight().isBoundToVariable()) {
					addFloatVariable(c.getWeight().getVariable(), oldFloatVariables);
				}
				if (c.getSpeed().isBoundToVariable()) {
					addFloatVariable(c.getSpeed().getVariable(), oldFloatVariables);
				}
				break;
			}
			case BLEND_SET_WEIGHT:
			{
				AnimationStateMachine.BlendSetWeightCommand c = (AnimationStateMachine.BlendSetWeightCommand) command;
				if (c.getWeight().isBoundToVariable()) {
					addFloatVariable(c.getWeight().getVariable(), oldFloatVariables);
				}
				break;
			}
			case SEQUENCE_TRANSITION_WEIGHT:
			{
				AnimationStateMachine.SequenceTransitionWeightCommand c = (AnimationStateMachine
						.SequenceTransitionWeightCommand) command;
				if (c.getWeight().isBoundToVariable()) {
					addFloatVariable(c.getWeight().getVariable(), oldFloatVariables);
				}
				if (c.getSpeed().isBoundToVariable()) {
					addFloatVariable(c.getSpeed().getVariable(), oldFloatVariables);
				}
				break;
			}
			case SEQUENCE_SET_WEIGHT:
			{
				AnimationStateMachine.SequenceSetWeightCommand c = (AnimationStateMachine.SequenceSetWeightCommand) command;
				if (c.getWeight().isBoundToVariable()) {
					addFloatVariable(c.getWeight().getVariable(), oldFloatVariables);
				}
				break;
			}
			case STATE_SET_ACTIVE:
				break;
			case EVENT_TRIGGER:
				break;
			default:
				throw new IllegalArgumentException("Invalid command type " + command.getType());
		}
	}

	private void addFloatVariable(String name, HashMap<String, Float> oldFloatVariables) {
		if (!floatVariables.containsKey(name)) {
			Float oldValue = oldFloatVariables.get(name);
			if (oldValue == null) {
				floatVariables.put(name, 0.0f);
			} else {
				floatVariables.put(name, oldValue);
			}
		}
	}

	private void addIntVariable(String name, HashMap<String, Integer> oldIntVariables) {
		if (!intVariables.containsKey(name)) {
			Integer oldValue = oldIntVariables.get(name);
			if (oldValue == null) {
				intVariables.put(name, 0);
			} else {
				intVariables.put(name, oldValue);
			}
		}
	}

	private final AnimationEditor editor;

	// PREVIEW FUNCTIONS

	public void selectBoneInPreview(int boneIndex) {}
	public void translateInPreview(Vector2f translation) {}
	public void finishTranslateInPreview() {}
	public void rotateInPreview(float rotation) {}
	public void finishRotateInPreview() {}
	public void scaleInPreview(Vector2f scale) {}
	public void finishScaleInPreview() {}
	public Vector2f getBoneTranslation(int boneIndex) { return null; }
	public float getBoneRotation(int boneIndex) { return 0.0f; }
	public Vector2f getBoneScale(int boneIndex) { return null; }
	public void enableBoneInAnimationInPreview() {}
	public void addKeyframeInPreview() {}
	public void prevKeyframeInPreview(boolean first) {}
	public void nextKeyframeInPreview(boolean last) {}

	public void frameBeginInPreview() {
		synchronized (editor) {
			if (treeValid && stateMachineValid) {
				final float dt = 1.0f / 60.0f;
				editor.state.update(dt);
				editor.evaluator.evaluate(editor.state);
				setStateActiveColors();
			}
		}
	}

	private void setStateActiveColors() {
		if (treeValid && stateMachineValid) {
			for (int i = 0; i < statesListModel.getSize(); ++i) {
				String state = statesListModel.get(i);
				boolean active = editor.state.isStateActive(state);
				Color oldCol = statesListRenderer.get(i);
				statesListRenderer.set(i, active ? ACTIVE_COLOR : INACTIVE_COLOR);
				if (!oldCol.equals(statesListRenderer.get(i))) {
					// force repaint of that row
					statesListModel.set(i, statesListModel.get(i));
				}
			}
		} else {
			// all states are inactive (but keep red for "error")
			for (int i = 0; i < statesListModel.getSize(); ++i) {
				if (!statesListRenderer.get(i).equals(ERROR_COLOR)) {
					statesListRenderer.set(i, INACTIVE_COLOR);
				}
			}
		}
	}
}
